package org.springframework.data.jpa.repository.support;

import com.biwin.core.api.enums.BwBasicExceptionEnum;
import com.biwin.module.orm.starter.exception.BwUnSupportException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import javax.persistence.*;
import javax.persistence.criteria.*;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * todo
 *
 * @author biwin
 * @since 0.0.1
 */
public class CustomJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, Serializable> {

    private final EntityManager entityManager;

//    private final SessionFactory sessionFactory;

    public CustomJpaRepository(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.entityManager = em;
//        this.sessionFactory = sessionFactory;
    }

    public Page<T> findAll(@Nullable Specification<T> spec, String[] groupBys, Pageable pageable) {
        TypedQuery<T> query = getQuery(spec, groupBys, pageable);
        return isUnpaged(pageable) ? new PageImpl<T>(query.getResultList())
                : readPage(query, getDomainClass(), pageable, spec);
    }

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, String[] groupBys, Pageable pageable) {
        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        return getQuery(spec, groupBys, getDomainClass(), sort);
    }

    public List<T> findAll(@Nullable Specification<T> spec, String[] groupBys, Sort sort) {
        return this.getQuery(spec, groupBys, sort).getResultList();
    }

    public List<T> findAll(@Nullable Specification<T> spec, String[] selects, String[] groupBys, Sort sort) {
        Class<T> domainClass = this.getDomainClass();
        TypedQuery<Tuple> query = this.getSelectedQuery(spec, selects, groupBys, domainClass, sort);
        List<Tuple> resultList = query.getResultList();
        List<T> result = resultList.stream().map(item -> {
            T t = null;
            try {
                t = domainClass.newInstance();
                for (int i = 0; i < selects.length; i++) {
                    String select = selects[i];
                    select = "set" + select.substring(0, 1).toUpperCase() + select.substring(1);
                    Object value = item.get(i);
                    Method method = domainClass.getMethod(select, value.getClass());
                    method.invoke(t, value);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return t;
        }).collect(Collectors.toList());
        return result;
    }

    public List<Long> findIds(@Nullable Specification<T> spec, String[] groupBys, Sort sort) {
        Class<T> domainClass = this.getDomainClass();
        TypedQuery<Tuple> query = this.getSelectedQuery(spec, new String[]{"id"}, groupBys, domainClass, sort);
        List<Tuple> resultList = query.getResultList();
        List<Long> result = resultList.stream().map(item -> {
            Object id = item.get(0);
            return Long.valueOf(id.toString());
        }).collect(Collectors.toList());
        return result;
    }

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, String[] groupBys, Sort sort) {
        return this.getQuery(spec, groupBys, this.getDomainClass(), sort);
    }

    protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, String[] groupBys,
                                                   Class<S> domainClass, Sort sort) {
        CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);
        Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
        query.select(root);
        if (groupBys != null && groupBys.length > 0) {
            Path[] duplicates = new Path[groupBys.length];
            int n = 0;
            for (String groupBy : groupBys) {
                duplicates[n] = root.get(groupBy);
                n++;
            }
            query.groupBy(duplicates);
        }
        if (sort.isSorted()) {
            query.orderBy(QueryUtils.toOrders(sort, root, builder));
        }

        TypedQuery<S> typedQuery = this.entityManager.createQuery(query);

        return this.applyRepositoryMethodMetadata(typedQuery);
    }

    protected <S extends T> TypedQuery<Tuple> getSelectedQuery(@Nullable Specification<S> spec, String[] fields,
                                                               String[] groupBys, Class<S> domainClass, Sort sort) {
        CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
        //列表查询
        CriteriaQuery<Tuple> query = builder.createTupleQuery();
        Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
        Selection[] selections = new Selection[fields.length];
        int n = 0;
        for (String field : fields) {
            selections[n] = root.get(field);
            n++;
        }
        query.multiselect(selections);
        if (groupBys != null && groupBys.length > 0) {
            Path[] duplicates = new Path[groupBys.length];
            int m = 0;
            for (String groupBy : groupBys) {
                duplicates[m] = root.get(groupBy);
                m++;
            }
            query.groupBy(duplicates);
        }
        if (sort.isSorted()) {
            query.orderBy(QueryUtils.toOrders(sort, root, builder));
        }

        TypedQuery<Tuple> typedQuery = this.entityManager.createQuery(query);

        return typedQuery;
    }

    protected TypedQuery<Tuple> getSumQuery(@Nullable Specification<T> spec, String[] fields) {
        return this.getSumQuery(spec, fields, this.getDomainClass());
    }

    protected <S extends T> TypedQuery<Tuple> getSumQuery(@Nullable Specification<S> spec, String[] fields,
                                                          Class<S> domainClass) {
        CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
        //统计查询
        CriteriaQuery<Tuple> query = builder.createTupleQuery();
        Root<S> root = this.applySpecificationToCriteria(spec, domainClass, query);
        if (fields != null && fields.length > 0) {
            Expression[] expressions = new Expression[fields.length];
            int n = 0;
            for (String field : fields) {
                expressions[n] = builder.sum(root.get(field));
                n++;
            }
            query.multiselect(expressions);
        }
        TypedQuery<Tuple> typedQuery = this.entityManager.createQuery(query);

        return typedQuery;
    }

    private static boolean isUnpaged(Pageable pageable) {
        return pageable.isUnpaged();
    }

    private <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec,
                                                                  Class<U> domainClass, CriteriaQuery<S> query) {
        Assert.notNull(domainClass, "Domain class must be not null!");
        Assert.notNull(query, "CriteriaQuery must be not null!");
        Root<U> root = query.from(domainClass);
        if (spec == null) {
            return root;
        } else {
            CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
            Predicate predicate = spec.toPredicate(root, query, builder);
            if (predicate != null) {
                query.where(predicate);
            }
            return root;
        }
    }

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
        if (getRepositoryMethodMetadata() == null) {
            return query;
        } else {
            LockModeType type = getRepositoryMethodMetadata().getLockModeType();
            TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);
            this.applyQueryHints(toReturn);
            return toReturn;
        }
    }


    protected void applyQueryHints(Query query) {
        for (Map.Entry<String, Object> hint : super.getQueryHints().withFetchGraphs(this.entityManager)) {
            query.setHint(hint.getKey(), hint.getValue());
        }
    }

    @Override
    public final void deleteById(Serializable serializable) {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }

    @Override
    public final void delete(T entity) {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }

    @Override
    public final void deleteAll(Iterable<? extends T> entities) {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }

    @Override
    public final void deleteInBatch(Iterable<T> entities) {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }

    @Override
    public final void deleteAll() {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }

    @Override
    public final void deleteAllInBatch() {
        throw new BwUnSupportException(BwBasicExceptionEnum.NOT_SUPPORTED);
    }
}
